﻿//  Copyright © 2009-2010 by Rhy A. Mednick
//  All rights reserved.
//  http://rhyduino.codeplex.com
//  
//  Redistribution and use in source and binary forms, with or without modification, 
//  are permitted provided that the following conditions are met:
//  
//  * Redistributions of source code must retain the above copyright notice, this list 
//    of conditions and the following disclaimer.
//  
//  * Redistributions in binary form must reproduce the above copyright notice, this 
//    list of conditions and the following disclaimer in the documentation and/or other 
//    materials provided with the distribution.
//  
//  * Neither the name of Rhy A. Mednick nor the names of its contributors may be used 
//    to endorse or promote products derived from this software without specific prior 
//    written permission.
//  
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
//  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
//  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
//  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
//  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
//  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
//  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
//  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
//  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
//  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
//  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;
using Rhyduino.Message;
using TracerX;

namespace Rhyduino
{
    /// <summary>
    ///   This class listens on the provided serial port and broadcasts the decoded
    ///   messages it receives. It makes no attempts to open or close the port.
    /// </summary>
    internal sealed class SerialListener : IDisposable
    {
        #region Public Fields

        public EventHandler<FirmataEventArgs<FirmataMessage>> PacketReceived;

        #endregion

        #region Constructor(s)

        public SerialListener(SerialPort serialPort, bool autoStart = false)
        {
            if (String.IsNullOrEmpty(Thread.CurrentThread.Name))
            {
                Thread.CurrentThread.Name = "SerialListener";
            }
            using (_log.DebugCall())
            {
                _serialPort = serialPort;
                _monitorTask = new Task(Monitor, _cancellationTokenSource.Token);

                if (autoStart)
                {
                    Start();
                }
            }
        }

        #endregion

        #region Public Methods

        public void Start()
        {
            using (_log.DebugCall())
            {
                _monitorTask.Start();
            }
        }

        #endregion

        #region Private Methods

        private static bool IsCommandByte(byte current)
        {
            // A byte from a message is considered a command if the highest 
            // bit is set. This is why data messages are formatted as 
            // 7-bit bytes.
            return ((current & 0x80) == 0x80);
        }

        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void Monitor()
        {
            using (_log.DebugCall())
            {
                // Continually poll serial port for data.
                // When data is present, post it to the serial queue
                // If byte read is 0xF7 (end of message) then kick off
                // process to parse out message.

                var deadline = DateTime.Now.AddSeconds(1);
                var wait = true;
                // Check every periodically for a cancellation token.
                while (wait)
                {
                    try
                    {
                        var bytesToRead = _serialPort.BytesToRead;
                        if ((bytesToRead == 0) && (_serialQueue.Count > 0))
                        {
                            ParseMessage();
                        }
                        for (var i = 0; i < bytesToRead; i++)
                        {
                            var data = (byte) _serialPort.ReadByte();
                            _serialQueue.Enqueue((byte) data);
                            _log.DebugFormat("[Serial Data] 0x{0:X2}", data);
                        }
                        _log.DebugFormat("Serial queue length: {0}", _serialQueue.Count);

                        if (DateTime.Now > deadline)
                        {
                            if (_cancellationTokenSource.IsCancellationRequested)
                            {
                                wait = false;
                                _log.DebugFormat("Serial monitor cancelled.");
                            }
                            deadline = DateTime.Now.AddSeconds(1);
                        }
                    }
                    catch (Exception)
                    {
                        break;
                    }
                }
            }
        }

        private void OnPacketReceived(FirmataMessage message)
        {
            using (_log.DebugCall())
            {
                _log.DebugFormat("Serial packet received: {0}", message.GetRawMessage().ToHexString());
                if (PacketReceived != null)
                {
                    PacketReceived(this, new FirmataEventArgs<FirmataMessage>(message));
                }
            }
        }

        private void ParseMessage()
        {
            using (_log.DebugCall())
            {
                _inSysex = false;

                while (_serialQueue.Count > 0)
                {
                    var current = _serialQueue.Dequeue();

                    switch (current)
                    {
                        case (byte) ResponseMessageType.SysexStart:
                            _inSysex = true;
                            _local.Add(current);
                            break;
                        case (byte) ResponseMessageType.SysexEnd:
                            _inSysex = false;
                            // process the local queue
                            _local.Add(current);
                            OnPacketReceived(new FirmataMessage(_local.ToArray()));
                            _local.Clear();
                            break;
                        default:
                            // If we're in a sysex block, just ignore the data
                            if (_inSysex)
                            {
                                _local.Add(current);
                            }
                            else
                            {
                                if (IsCommandByte(current) && _local.Count > 0)
                                {
                                    // If it's a command byte and the cache has data, raise the event and flush the cache
                                    OnPacketReceived(new FirmataMessage(_local.ToArray()));
                                    _local.Clear();
                                }
                                _local.Add(current);
                            }
                            break;
                    }
                }

                // If we (a) still have data in the local queue, we need to check if it's a complete message.
                if (_local.Count == 0) return;
                // However, this section of code has very broad rules regarding what makes a message.
                // At this point in the program flow, only non-sysex messages could be valid, so we can
                // (b) eliminate any sysex messages.
                if (_inSysex) return;
                // We can also (c) eliminate any blocks of data in which the first bit isn't a command bit.
                if (!IsCommandByte(_local[0])) return;
                // Finally, all messages that might be valid at this point are going to be (d) three bytes wide.
                if (_local.Count != 3) return;
                // If we've made it here, the remaining data is probably a valid message, so send it off,
                OnPacketReceived(new FirmataMessage(_local.ToArray()));
                // and flush the local cache.
                _local.Clear();
            }
        }

        private void Stop()
        {
            using (_log.DebugCall())
            {
                if (_monitorTask == null)
                {
                    return;
                }
                _cancellationTokenSource.Cancel();
                while (!_monitorTask.IsFaulted && !_monitorTask.IsCompleted && !_monitorTask.IsCanceled)
                {
                }
                _monitorTask.Dispose();
                _monitorTask = null;
            }
        }

        #endregion

        #region Implementation of IDisposable interface

        #region Public Methods

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion

        #region Private Fields

        private bool _disposed;

        #endregion

        private void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }
            if (!disposing)
            {
                return;
            }

            Stop();
            _cancellationTokenSource.Dispose();
            // All done
            _disposed = true;
        }

        ~SerialListener()
        {
            Dispose(false);
        }

        #endregion

        #region Private Fields

        private static readonly Logger _log = Logger.GetLogger("SerialListener");

        private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
        private readonly List<byte> _local = new List<byte>();
        private readonly SerialPort _serialPort;
        private readonly Queue<byte> _serialQueue = new Queue<byte>();
        private bool _inSysex;
        private Task _monitorTask;

        #endregion
    }
}